// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include "environment.h"

#include "algorithm.h"
#include "filepath.h"
#include "qtcassert.h"

#include <QDir>
#include <QProcessEnvironment>
#include <QReadWriteLock>
#include <QSet>

/*!
    \class Utils::Environment
    \inmodule QtCreator

    \brief The Environment class sets \QC's system environment.
*/

namespace Utils {

static QReadWriteLock s_envMutex;
Q_GLOBAL_STATIC_WITH_ARGS(Environment, staticSystemEnvironment,
                          (QProcessEnvironment::systemEnvironment().toStringList()))
Q_GLOBAL_STATIC(QList<EnvironmentProvider>, environmentProviders)

Environment::Environment()
    : m_dict(HostOsInfo::hostOs())
{}

Environment::Environment(OsType osType)
    : m_dict(osType)
{}

Environment::Environment(const QStringList &env, OsType osType)
    : m_dict(osType)
{
    m_changeItems.append(NameValueDictionary(env, osType));
}

Environment::Environment(const NameValuePairs &nameValues)
{
    m_changeItems.append(NameValueDictionary(nameValues));
}

Environment::Environment(const NameValueDictionary &dict)
{
    m_changeItems.append(dict);
}

EnvironmentItems Environment::diff(const Environment &other, bool checkAppendPrepend) const
{
    const NameValueDictionary &dict = resolved();
    const NameValueDictionary &otherDict = other.resolved();
    return dict.diff(otherDict, checkAppendPrepend);
}

Environment::FindResult Environment::find(const QString &name) const
{
    const NameValueDictionary &dict = resolved();
    const auto it = dict.find(name);
    if (it == dict.end())
        return {};
    return Entry{it.key(), it.value(), it.enabled()};
}

void Environment::forEachEntry(const std::function<void(const QString &, const QString &, bool)> &callBack) const
{
    const NameValueDictionary &dict = resolved();
    for (auto it = dict.m_values.constBegin(); it != dict.m_values.constEnd(); ++it)
        callBack(it.key().name, it.value().first, it.value().second);
}

bool Environment::operator==(const Environment &other) const
{
    const NameValueDictionary &dict = resolved();
    const NameValueDictionary &otherDict = other.resolved();
    return dict == otherDict;
}

bool Environment::operator!=(const Environment &other) const
{
    const NameValueDictionary &dict = resolved();
    const NameValueDictionary &otherDict = other.resolved();
    return dict != otherDict;
}

QString Environment::value(const QString &key) const
{
    const NameValueDictionary &dict = resolved();
    return dict.value(key);
}

QString Environment::value_or(const QString &key, const QString &defaultValue) const
{
    const NameValueDictionary &dict = resolved();
    return dict.hasKey(key) ? dict.value(key) : defaultValue;
}

bool Environment::hasKey(const QString &key) const
{
    const NameValueDictionary &dict = resolved();
    return dict.hasKey(key);
}

bool Environment::hasChanges() const
{
    const NameValueDictionary &dict = resolved();
    return dict.size() != 0;
}

OsType Environment::osType() const
{
    return m_dict.m_osType;
}

QStringList Environment::toStringList() const
{
    const NameValueDictionary &dict = resolved();
    return dict.toStringList();
}

QProcessEnvironment Environment::toProcessEnvironment() const
{
    QProcessEnvironment result;
    for (const auto &[key, _, enabled] : resolved()) {
        if (enabled)
            result.insert(key, expandedValueForKey(key));
    }
    return result;
}

void Environment::appendOrSetPath(const FilePath &value)
{
    QTC_CHECK(value.osType() == m_dict.m_osType);
    if (value.isEmpty())
        return;
    appendOrSet("PATH", value.nativePath());
}

void Environment::prependOrSetPath(const FilePath &value)
{
    QTC_CHECK(value.osType() == m_dict.m_osType);
    if (value.isEmpty())
        return;
    prependOrSet("PATH", value.nativePath());
}

void Environment::prependOrSetPath(const QString &directories)
{
    prependOrSet("PATH", directories);
}

void Environment::appendOrSet(const QString &key, const QString &value, PathSeparator sep)
{
    addItem(Item{std::in_place_index_t<AppendOrSet>(), key, value, sep});
}

void Environment::prependOrSet(const QString &key, const QString &value, PathSeparator sep)
{
    addItem(Item{std::in_place_index_t<PrependOrSet>(), key, value, sep});
}

void Environment::prependOrSetLibrarySearchPath(const FilePath &value)
{
    QTC_CHECK(value.osType() == osType());
    switch (osType()) {
    case OsTypeWindows: {
        prependOrSet("PATH", value.nativePath());
        break;
    }
    case OsTypeMac: {
        const QString nativeValue = value.nativePath();
        prependOrSet("DYLD_LIBRARY_PATH", nativeValue);
        prependOrSet("DYLD_FRAMEWORK_PATH", nativeValue);
        break;
    }
    case OsTypeLinux:
    case OsTypeOtherUnix: {
        prependOrSet("LD_LIBRARY_PATH", value.nativePath());
        break;
    }
    default:
        break;
    }
}

void Environment::prependOrSetLibrarySearchPaths(const FilePaths &values)
{
    reverseForeach(values, [this](const FilePath &value) {
        prependOrSetLibrarySearchPath(value);
    });
}

/*!
    Returns \QC's system environment.

    This can be different from the system environment that \QC started in if the
    user changed it in \uicontrol Preferences > \uicontrol Environment >
    \uicontrol System > \uicontrol Environment.
*/

Environment Environment::systemEnvironment()
{
    QReadLocker lock(&s_envMutex);
    return *staticSystemEnvironment();
}

const Environment &Environment::originalSystemEnvironment()
{
    static const Environment env(QProcessEnvironment::systemEnvironment().toStringList());
    return env;
}

void Environment::setupEnglishOutput()
{
    addItem(Item{std::in_place_index_t<SetupEnglishOutput>()});
}

using SearchResultCallback = std::function<IterationPolicy(const FilePath &)>;

QString Environment::expandedValueForKey(const QString &key) const
{
    const NameValueDictionary &dict = resolved();
    return expandVariables(dict.value(key));
}

FilePath Environment::searchInPath(const QString &executable,
                                   const FilePaths &additionalDirs,
                                   const FilePathPredicate &filter,
                                   FilePath::MatchScope scope) const
{
    const FilePath exec = FilePath::fromUserInput(expandVariables(executable));
    const FilePaths dirs = path() + additionalDirs;
    return exec.searchInDirectories(dirs, filter, scope);
}

FilePaths Environment::path() const
{
    return pathListValue("PATH");
}

FilePaths Environment::pathListValue(const QString &varName) const
{
    return pathListFromValue(expandedValueForKey(varName), osType());
}

void Environment::setPathListValue(const QString &varName, const FilePaths &paths)
{
    set(varName, valueFromPathList(paths, osType()));
}

QString Environment::valueFromPathList(const FilePaths &paths, OsType osType)
{
    return transform(paths, &FilePath::toUserOutput)
        .join(OsSpecificAspects::pathListSeparator(osType));
}

FilePaths Environment::pathListFromValue(const QString &value, OsType osType)
{
    const QStringList pathComponents
        = value.split(OsSpecificAspects::pathListSeparator(osType), Qt::SkipEmptyParts);
    return transform(pathComponents, &FilePath::fromUserInput);
}

void Environment::modifySystemEnvironment(const EnvironmentItems &list)
{
    QWriteLocker lock(&s_envMutex);
    staticSystemEnvironment->modify(list);
}

void Environment::setSystemEnvironment(const Environment &environment)
{
    QWriteLocker lock(&s_envMutex);
    *staticSystemEnvironment = environment;
}

/** Expand environment variables in a string.
 *
 * Environment variables are accepted in the following forms:
 * $SOMEVAR, ${SOMEVAR} on Unix and %SOMEVAR% on Windows.
 * No escapes and quoting are supported.
 * If a variable is not found, it is not substituted.
 */
QString Environment::expandVariables(const QString &input) const
{
    const NameValueDictionary &dict = resolved();

    QString result = input;

    if (osType() == OsTypeWindows) {
        for (int vStart = -1, i = 0; i < result.length(); ) {
            if (result.at(i++) == '%') {
                if (vStart > 0) {
                    const auto it = dict.findKey(result.mid(vStart, i - vStart - 1));
                    if (it != dict.m_values.constEnd()) {
                        result.replace(vStart - 1, i - vStart + 1, it->first);
                        i = vStart - 1 + it->first.length();
                        vStart = -1;
                    } else {
                        vStart = i;
                    }
                } else {
                    vStart = i;
                }
            }
        }
    } else {
        enum { BASE, OPTIONALVARIABLEBRACE, VARIABLE, BRACEDVARIABLE } state = BASE;
        int vStart = -1;

        for (int i = 0; i < result.length();) {
            QChar c = result.at(i++);
            if (state == BASE) {
                if (c == '$')
                    state = OPTIONALVARIABLEBRACE;
            } else if (state == OPTIONALVARIABLEBRACE) {
                if (c == '{') {
                    state = BRACEDVARIABLE;
                    vStart = i;
                } else if (c.isLetterOrNumber() || c == '_') {
                    state = VARIABLE;
                    vStart = i - 1;
                } else {
                    state = BASE;
                }
            } else if (state == BRACEDVARIABLE) {
                if (c == '}') {
                    const QString key = result.mid(vStart, i - 1 - vStart);
                    const Environment::FindResult res = find(key);
                    if (res) {
                        result.replace(vStart - 2, i - vStart + 2, res->value);
                        i = vStart - 2 + res->value.length();
                    }
                    state = BASE;
                }
            } else if (state == VARIABLE) {
                if (!c.isLetterOrNumber() && c != '_') {
                    const QString key = result.mid(vStart, i - vStart - 1);
                    const Environment::FindResult res = find(key);
                    if (res) {
                        result.replace(vStart - 1, i - vStart, res->value);
                        i = vStart - 1 + res->value.length();
                    }
                    state = BASE;
                }
            }
        }
        if (state == VARIABLE) {
            const Environment::FindResult res = find(result.mid(vStart));
            if (res)
                result.replace(vStart - 1, result.length() - vStart + 1, res->value);
        }
    }
    return result;
}

FilePath Environment::expandVariables(const FilePath &variables) const
{
    return FilePath::fromUserInput(expandVariables(variables.toUrlishString()));
}

QStringList Environment::expandVariables(const QStringList &variables) const
{
    return transform(variables, [this](const QString &i) { return expandVariables(i); });
}

NameValueDictionary Environment::toDictionary() const
{
    const NameValueDictionary &dict = resolved();
    return dict;
}

void EnvironmentProvider::addProvider(EnvironmentProvider &&provider)
{
    environmentProviders->append(std::move(provider));
}

const QList<EnvironmentProvider> EnvironmentProvider::providers()
{
    return *environmentProviders;
}

std::optional<EnvironmentProvider> EnvironmentProvider::provider(const QByteArray &id)
{
    const int index = indexOf(*environmentProviders, equal(&EnvironmentProvider::id, id));
    if (index >= 0)
        return std::make_optional(environmentProviders->at(index));
    return std::nullopt;
}

void Environment::addItem(const Item &item)
{
    m_dict.clear();
    m_changeItems.append(item);
}

void Environment::set(const QString &key, const QString &value, bool enabled)
{
    addItem(Item{std::in_place_index_t<SetValue>(),
                 std::tuple<QString, QString, bool>{key, value, enabled}});
}

void Environment::setFallback(const QString &key, const QString &value)
{
    addItem(Item{std::in_place_index_t<SetFallbackValue>(),
                 std::tuple<QString, QString>{key, value}});
}

void Environment::unset(const QString &key)
{
    addItem(Item{std::in_place_index_t<UnsetValue>(), key});
}

void Environment::modify(const EnvironmentItems &items)
{
    addItem(Item{std::in_place_index_t<Modify>(), items});
}

void Environment::prependToPath(const FilePaths &values)
{
    m_dict.clear();
    for (int i = values.size(); --i >= 0; ) {
        const FilePath value = values.at(i);
        m_changeItems.append(Item{std::in_place_index_t<PrependOrSet>(),
                                  QString("PATH"),
                                  value.nativePath(),
                                  PathSeparator::Auto});
    }
}

void Environment::appendToPath(const FilePaths &values)
{
    m_dict.clear();
    for (const FilePath &value : values) {
        m_changeItems.append(Item{std::in_place_index_t<AppendOrSet>(),
                                  QString("PATH"),
                                  value.nativePath(),
                                  PathSeparator::Auto});
    }
}

const NameValueDictionary &Environment::resolved() const
{
    if (m_dict.size() != 0)
        return m_dict;

    m_fullDict = false;
    for (const Item &item : m_changeItems) {
        switch (item.index()) {
        case SetSystemEnvironment:
            m_dict = Environment::systemEnvironment().toDictionary();
            m_fullDict = true;
            break;
        case SetFixedDictionary: {
            const auto dict = std::get_if<SetFixedDictionary>(&item);
            if (QTC_GUARD(dict)) {
                m_dict = *dict;
                m_fullDict = true;
            }
            break;
        }
        case SetValue: {
            const auto setvalue = std::get_if<SetValue>(&item);
            if (QTC_GUARD(setvalue)) {
                auto [key, value, enabled] = *setvalue;
                m_dict.set(key, value, enabled);
            }
            break;
        }
        case SetFallbackValue: {
            const auto fallbackvalue = std::get_if<SetFallbackValue>(&item);
            if (QTC_GUARD(fallbackvalue)) {
                auto [key, value] = *fallbackvalue;
                if (m_fullDict) {
                    if (m_dict.value(key).isEmpty())
                        m_dict.set(key, value, true);
                } else {
                    QTC_ASSERT(false, qDebug() << "operating on partial dictionary");
                    m_dict.set(key, value, true);
                }
            }
            break;
        }
        case UnsetValue: {
            const auto unsetvalue = std::get_if<UnsetValue>(&item);
            if (QTC_GUARD(unsetvalue))
                m_dict.unset(*unsetvalue);
            break;
        }
        case PrependOrSet: {
            const auto prependorset = std::get_if<PrependOrSet>(&item);
            if (QTC_GUARD(prependorset)) {
                auto [key, value, sep] = *prependorset;
                QTC_ASSERT(!key.contains('='), return m_dict);
                const auto it = m_dict.findKey(key);
                if (it == m_dict.m_values.end()) {
                    m_dict.m_values.insert(DictKey(key, m_dict.nameCaseSensitivity()), {value, true});
                } else {
                    // Prepend unless it is already there
                    const QString toPrepend = value + pathListSeparator(sep);
                    if (!it.value().first.startsWith(toPrepend))
                        it.value().first.prepend(toPrepend);
                }
            }
            break;
        }
        case AppendOrSet: {
            const auto appendorset = std::get_if<AppendOrSet>(&item);
            if (QTC_GUARD(appendorset)) {
                auto [key, value, sep] = *appendorset;
                QTC_ASSERT(!key.contains('='), return m_dict);
                const auto it = m_dict.findKey(key);
                if (it == m_dict.m_values.end()) {
                    m_dict.m_values.insert(DictKey(key, m_dict.nameCaseSensitivity()), {value, true});
                } else {
                    // Prepend unless it is already there
                    const QString toAppend = pathListSeparator(sep) + value;
                    if (!it.value().first.endsWith(toAppend))
                        it.value().first.append(toAppend);
                }
            }
            break;
        }
        case Modify: {
            const auto modify = std::get_if<Modify>(&item);
            if (QTC_GUARD(modify)) {
                EnvironmentItems items = *modify;
                m_dict.modify(items);
            }
            break;
        }
        case SetupEnglishOutput:
            m_dict.set("LC_MESSAGES", "en_US.utf8");
            m_dict.set("LANGUAGE", "en_US:en");
            break;
        }
    }

    return m_dict;
}

Environment Environment::appliedToEnvironment(const Environment &base) const
{
    Environment res = base;
    res.m_dict.clear();
    res.m_changeItems.append(m_changeItems);
    return res;
}

QChar Environment::pathListSeparator(PathSeparator sep) const
{
    if (sep == PathSeparator::Semicolon)
        return QLatin1Char(';');
    else if (sep == PathSeparator::Colon)
        return QLatin1Char(':');
    return OsSpecificAspects::pathListSeparator(osType());
}

/*!
    Returns the value of \a key in \QC's modified system environment.
    \sa Utils::Environment::systemEnvironment
    \sa qEnvironmentVariable
*/
QString qtcEnvironmentVariable(const QString &key)
{
    return Environment::systemEnvironment().value(key);
}

/*!
    Returns the value of \a key in \QC's modified system environment if it is set,
    or otherwise \a defaultValue.
    \sa Utils::Environment::systemEnvironment
    \sa qEnvironmentVariable
*/
QString qtcEnvironmentVariable(const QString &key, const QString &defaultValue)
{
    if (Environment::systemEnvironment().hasKey(key))
        return Environment::systemEnvironment().value(key);
    return defaultValue;
}

/*!
    Returns if the environment variable \a key is set \QC's modified system environment.
    \sa Utils::Environment::systemEnvironment
    \sa qEnvironmentVariableIsSet
*/
bool qtcEnvironmentVariableIsSet(const QString &key)
{
    return Environment::systemEnvironment().hasKey(key);
}

/*!
    Returns if the environment variable \a key is not set or empty in \QC's modified system
    environment.
    \sa Utils::Environment::systemEnvironment
    \sa qEnvironmentVariableIsEmpty
*/
bool qtcEnvironmentVariableIsEmpty(const QString &key)
{
    return Environment::systemEnvironment().value(key).isEmpty();
}

/*!
    Returns the value of \a key in \QC's modified system environment, converted to an int.
    If \a ok is not null, sets \c{*ok} to true or false depending on the success of the conversion
    \sa Utils::Environment::systemEnvironment
    \sa qEnvironmentVariableIntValue
*/
int qtcEnvironmentVariableIntValue(const QString &key, bool *ok)
{
    return Environment::systemEnvironment().value(key).toInt(ok);
}

} // namespace Utils
